package pushtest.mobike.com.mtpush.ui.behavior;

import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;

import android.content.Context;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.CoordinatorLayout.Behavior;
import android.support.v4.view.AbsSavedState;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.NestedScrollView;
import android.support.v4.widget.ViewDragHelper;
import android.support.v4.widget.ViewDragHelper.Callback;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import java.lang.ref.WeakReference;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber;

public class EasyBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {

  public static final int STATE_DRAGGING = 1;
  public static final int STATE_SETTLING = 2;
  public static final int STATE_EXPANDED = 3;
  public static final int STATE_EXPANDED_MANUAL = -STATE_EXPANDED;
  public static final int STATE_COLLAPSED = 4;
  public static final int STATE_ADD_TOP_VIEW = 5;


  private static final int DEFAULT_PEEK_HEIGHT = 400;//px
  private boolean fitToContents = true;
  private int fitToContentsOffset;
  private float maximumVelocity;
  private int peekHeight;
  private int collapsedOffset;
  private int state = STATE_COLLAPSED;
  private ViewDragHelper viewDragHelper;

  private boolean nestedScrolled;
  private int parentHeight;

  WeakReference viewRef;
  WeakReference<View> nestedScrollingChildRef;

  private VelocityTracker velocityTracker;
  private int initialY;
  private boolean touchingScrollingChild;
  private int lastPeekHeight;

  private int activePointerId;
  private BottomSheetCallback callback;
  private boolean ignoreEvents;
  private int lastNestedScrollDy;
  private int minVelocity;

  public EasyBehavior() {
  }

  public EasyBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
    TypedArray typedArray = context.obtainStyledAttributes(attrs,
        android.support.design.R.styleable.BottomSheetBehavior_Layout);

    TypedValue value = typedArray.peekValue(
        android.support.design.R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight);
    this.setPeekHeight(
        typedArray.getDimensionPixelSize(
            android.support.design.R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, -1));

    this.setFitToContents(
        typedArray.getBoolean(
            android.support.design.R.styleable.BottomSheetBehavior_Layout_behavior_fitToContents,
            true));
    typedArray.recycle();
    ViewConfiguration configuration = ViewConfiguration.get(context);
    this.maximumVelocity = (float) configuration.getScaledMaximumFlingVelocity();
    this.minVelocity = configuration.getScaledMinimumFlingVelocity();

  }

  public void setPeekHeight(int value) {
    if (value == -1) {
      value = DEFAULT_PEEK_HEIGHT;
    }
    if (this.peekHeight != value) {
      this.peekHeight = value;
      if (this.viewRef != null) {
        V view = (V) this.viewRef.get();
        if (view != null) {
          view.requestLayout();
        }
      }
    }

  }

  public void setFitToContents(boolean aBoolean) {
    if (this.fitToContentsOffset != activePointerId) {
      this.fitToContents = aBoolean;
      if (this.viewRef != null) {
        this.calculateCollapsedOffset();
      }
    }

  }

  @Override
  public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child,
      int layoutDirection) {
    if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) {
      child.setFitsSystemWindows(true);
    }
    int savedTop = child.getTop();
    parent.onLayoutChild(child, layoutDirection);
    this.parentHeight = parent.getHeight();
    this.lastPeekHeight = peekHeight;
    this.fitToContentsOffset = Math.max(0, this.parentHeight - child.getHeight());
    calculateCollapsedOffset();
    switch (state) {
      case STATE_COLLAPSED:
        ViewCompat.offsetTopAndBottom(child, this.collapsedOffset);
        break;
      case STATE_EXPANDED:
      case STATE_EXPANDED_MANUAL:
        ViewCompat.offsetTopAndBottom(child, this.getExpandedOffset());
        break;
      case STATE_DRAGGING:
      case STATE_SETTLING:
        ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
        break;
      case STATE_ADD_TOP_VIEW:
        ViewCompat.offsetTopAndBottom(child, this.getExpandedOffset());
        if (child.getTop() > collapsedOffset) {
          setStateInternal(STATE_EXPANDED_MANUAL);
        } else {
          setStateInternal(STATE_COLLAPSED);
        }
        break;
    }
    if (this.viewDragHelper == null) {
      this.viewDragHelper = ViewDragHelper.create(parent, dragCallBack);
    }
    this.viewRef = new WeakReference(child);
    this.nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));

    return true;
  }

  private View findScrollingChild(View child) {
    if (ViewCompat.isNestedScrollingEnabled(child)) {
      return child;
    } else {

      if (child instanceof ViewGroup) {
        for (int i = 0; i < ((ViewGroup) child).getChildCount(); i++) {
          View scrollingCh = findScrollingChild(((ViewGroup) child).getChildAt(i));
          if (scrollingCh != null) {
            return scrollingCh;
          }
        }

      }
      return null;
    }
  }

  @NonNull
  private Callback dragCallBack = new Callback() {
    @Override
    public boolean tryCaptureView(@NonNull View view, int i) {
      if (state == STATE_DRAGGING) {
        return false;
      } else if (touchingScrollingChild) {
        return false;
      } else {
        if (state == STATE_EXPANDED && activePointerId == i) {
          View scroll = nestedScrollingChildRef.get();
          if (scroll != null && scroll.canScrollVertically(-1)) {
            return false;
          }

        }
      }
      return viewRef != null && viewRef.get() == view;

    }

    @Override
    public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,
        int dy) {
      dispatchOnSlide(top);
    }

    @Override
    public void onViewDragStateChanged(int state) {
      if (state == STATE_DRAGGING) {
        setStateInternal(STATE_DRAGGING);
      }
    }

    @Override
    public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {

      int top;
      byte targetState = 0;
      int currentTop;
      if (yvel < 0.0f) {
        if (fitToContents) {
          top = fitToContentsOffset;
          targetState = STATE_EXPANDED;
        } else {
          currentTop = releasedChild.getTop();
          top = 0;
          targetState = STATE_EXPANDED;
        }

      } else {
        if (yvel != 0.0F && Math.abs(xvel) <= Math.abs(yvel)) {
          top = collapsedOffset;
          targetState = STATE_COLLAPSED;
        } else {
          currentTop = releasedChild.getTop();
          if (fitToContents) {
            if (Math.abs(currentTop - fitToContentsOffset) < Math
                .abs(currentTop - collapsedOffset)) {
              top = fitToContentsOffset;
              targetState = STATE_EXPANDED;
            } else {
              top = collapsedOffset;
              targetState = STATE_COLLAPSED;
            }
          } else {
            top = collapsedOffset;
            targetState = STATE_COLLAPSED;
          }
        }
      }
      if (viewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
        setStateInternal(STATE_SETTLING);
        ViewCompat.postOnAnimation(releasedChild,
            new SettleRunnable(releasedChild, targetState));
      } else {
        setStateInternal(targetState);
      }

    }

    @Override
    public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
      return android.support.v4.math.MathUtils.clamp(top, getExpandedOffset(), collapsedOffset);
    }

    @Override
    public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {

      return child.getLeft();
    }

    @Override
    public int getViewVerticalDragRange(@NonNull View child) {
      return collapsedOffset;
    }
  };

  @Nullable
  @Override
  public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child) {
    return new SavedState(super.onSaveInstanceState(parent, child), this.state);
  }

  @Override
  public void onRestoreInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child,
      @NonNull Parcelable state) {
    SavedState savedState = (SavedState) state;
    super.onRestoreInstanceState(parent, child, savedState.getSuperState());
    if (savedState.state != STATE_DRAGGING && savedState.state != STATE_SETTLING) {
      this.state = savedState.state;
    } else {
      this.state = STATE_COLLAPSED;
    }

  }

  @Override
  public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
      @NonNull MotionEvent ev) {
    if (!child.isShown()) {
      this.ignoreEvents = true;
      return false;
    } else {
      int action = ev.getActionMasked();
      if (ACTION_DOWN == action) {
        reset();
      }
      if (this.velocityTracker == null) {
        this.velocityTracker = VelocityTracker.obtain();
      }
      this.velocityTracker.addMovement(ev);

      switch (action) {
        case ACTION_DOWN:
          int initX = (int) ev.getX();
          this.initialY = (int) ev.getY();
          View scroll =
              this.nestedScrollingChildRef != null ? this.nestedScrollingChildRef.get() : null;

          if (scroll != null && parent.isPointInChildBounds(scroll, initX, initialY)) {
            this.activePointerId = ev.getPointerId(ev.getActionIndex());
            this.touchingScrollingChild = true;
          }
          this.ignoreEvents =
              this.activePointerId == -1 && !parent.isPointInChildBounds(child, initX, initialY);
          break;
        case ACTION_UP:
        case ACTION_CANCEL:
          this.touchingScrollingChild = false;
          this.activePointerId = -1;
          if (this.ignoreEvents) {
            ignoreEvents = false;
            return false;
          }
          break;
        case ACTION_MOVE:
      }
      if (!this.ignoreEvents && this.viewDragHelper != null && this.viewDragHelper
          .shouldInterceptTouchEvent(ev)) {
        return true;
      } else {
        View scroll = this.nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
        return action == ACTION_MOVE && scroll != null && !ignoreEvents && state != STATE_DRAGGING
            && !parent.isPointInChildBounds(scroll, (int) ev.getX(), (int) ev.getY())
            && this.viewDragHelper != null
            && Math
            .abs(((float) this.initialY - ev.getY())) > (float) this.viewDragHelper.getTouchSlop();
      }

    }
  }

  @Override
  public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child,
      @NonNull MotionEvent ev) {
    if (!child.isShown()) {
      return false;
    } else {
      int action = ev.getActionMasked();
      if (this.state == STATE_DRAGGING && action == ACTION_DOWN) {
        return true;
      } else {
        if (this.viewDragHelper != null) {
          this.viewDragHelper.processTouchEvent(ev);
        }
        if (action == ACTION_DOWN) {
          this.reset();
        }
        if (this.velocityTracker == null) {
          this.velocityTracker = VelocityTracker.obtain();
        }

        this.velocityTracker.addMovement(ev);

        if (action == STATE_SETTLING && !this.ignoreEvents
            && Math.abs((float) this.initialY - ev.getY()) > (float) this.viewDragHelper
            .getTouchSlop()) {
          this.viewDragHelper.captureChildView(child, ev.getPointerId(ev.getActionIndex()));
        }

        return !this.ignoreEvents;
      }
    }
  }

  @Override
  public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
      @NonNull View directTargetChild, @NonNull View target, int axes, int type) {

    this.lastNestedScrollDy = 0;
    this.nestedScrolled = false;
    return (axes & STATE_SETTLING) != 0;
  }

  @Override
  public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
      @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {

    if (type != STATE_DRAGGING) {
      View scrollingChild = (View) this.nestedScrollingChildRef.get();
      if (target == scrollingChild) {
        int currentTop = child.getTop();
        int newTop = currentTop - dy;
        if (dy > 0) {
          if (newTop < this.getExpandedOffset()) {
            consumed[STATE_DRAGGING] = currentTop - this.getExpandedOffset();
            ViewCompat.offsetTopAndBottom(child, -consumed[STATE_DRAGGING]);
            this.setStateInternal(STATE_EXPANDED);
          } else {
            consumed[STATE_DRAGGING] = dy;
            ViewCompat.offsetTopAndBottom(child, -dy);
            this.setStateInternal(STATE_DRAGGING);
          }
        } else if (dy < 0 && !target.canScrollVertically(-1)) {
          if (newTop > this.collapsedOffset) {
            consumed[STATE_DRAGGING] = currentTop - this.collapsedOffset;
            ViewCompat.offsetTopAndBottom(child, -consumed[STATE_DRAGGING]);
            this.setStateInternal(STATE_COLLAPSED);
          } else {
            consumed[STATE_DRAGGING] = dy;
            ViewCompat.offsetTopAndBottom(child, -dy);
            this.setStateInternal(STATE_DRAGGING);
          }
        }

        this.dispatchOnSlide(child.getTop());
        this.lastNestedScrollDy = dy;
        this.nestedScrolled = true;
      }
    }
  }

  @Override
  public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
      @NonNull View target, int type) {
    if (child.getTop() == this.getExpandedOffset()) {
      this.setStateInternal(STATE_EXPANDED);
    } else if (target == this.nestedScrollingChildRef.get() && this.nestedScrolled) {
      int top;
      byte targetState;
      if (this.lastNestedScrollDy > 0) {
        top = this.getExpandedOffset();
        targetState = STATE_EXPANDED;
      } else if (this.lastNestedScrollDy == 0) {
        int currentTop = child.getTop();
        if (this.fitToContents) {
          if (Math.abs(getYVelocity()) > minVelocity * 2) {
            if (Math.abs(currentTop - this.fitToContentsOffset) < Math
                .abs(currentTop - this.collapsedOffset)) {
              top = this.fitToContentsOffset;
              targetState = STATE_EXPANDED;
            } else {
              top = this.collapsedOffset;
              targetState = STATE_COLLAPSED;
            }
          } else {
            top = currentTop;
            targetState = STATE_EXPANDED_MANUAL;
          }
        } else {
          top = this.collapsedOffset;
          targetState = STATE_COLLAPSED;
        }
      } else {
        top = this.collapsedOffset;
        targetState = STATE_COLLAPSED;
      }
      Timber.i(" velocity = %s", getYVelocity());
      if (this.viewDragHelper
          .smoothSlideViewTo(child, child.getLeft(), top)) {
        this.setStateInternal(STATE_SETTLING);
        ViewCompat
            .postOnAnimation(child, new SettleRunnable(child, targetState));
      } else {
        this.setStateInternal(targetState);
      }

      this.nestedScrolled = false;
    }

  }

  public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
      @NonNull View target, float velocityX, float velocityY) {
    return target == this.nestedScrollingChildRef.get() && (this.state != STATE_EXPANDED || super
        .onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY));

  }

  private float getYVelocity() {
    if (this.velocityTracker == null) {
      return 0.0F;
    } else {
      this.velocityTracker.computeCurrentVelocity(100, this.maximumVelocity);
      return this.velocityTracker.getYVelocity(this.activePointerId);
    }
  }

  private void reset() {
    this.activePointerId = -1;
    if (this.velocityTracker != null) {
      this.velocityTracker.recycle();
      this.velocityTracker = null;
    }
  }

  private void setStateInternal(int state) {
    if (this.state != state) {
      this.state = state;
      View sheet = (View) viewRef.get();
      if (sheet != null && this.callback != null) {
        this.callback.onStateChanged(sheet, state);
      }
    }

  }

  private void dispatchOnSlide(int top) {
    View sheet = (View) viewRef.get();

    if (sheet != null && callback != null) {
      if (top > this.collapsedOffset) {
        this.callback.onSlide(sheet,
            (float) (this.collapsedOffset - top) / (float) (this.parentHeight
                - this.collapsedOffset));
      } else {
        this.callback.onSlide(sheet,
            (float) (this.collapsedOffset - top) / (float) (this.collapsedOffset - this
                .getExpandedOffset()));
      }
    }
  }

  private int getExpandedOffset() {
    return this.fitToContents ? fitToContentsOffset : 0;
  }

  private void calculateCollapsedOffset() {
    if (this.fitToContents) {
      this.collapsedOffset = Math
          .max(this.parentHeight - this.lastPeekHeight, this.fitToContentsOffset);
    } else {
      this.collapsedOffset = this.parentHeight - this.lastPeekHeight;
    }

  }

  public boolean isFitToContents() {
    return fitToContents;
  }

  public int getCollapsedOffset() {
    return collapsedOffset;
  }

  public void setCollapsedOffset(int collapsedOffset) {
    this.collapsedOffset = collapsedOffset;
  }

  public int getState() {
    return state;
  }

  public void setState(final int state) {
    if (this.state != state) {
      this.state = state;
      if (this.viewRef == null) {
        if (state == STATE_COLLAPSED || state == STATE_EXPANDED) {
          this.state = state;
        }
      } else {
        final V view = (V) viewRef.get();
        if (view != null) {
          ViewParent parent = view.getParent();
          if (parent != null && parent.isLayoutRequested() && ViewCompat
              .isImportantForAccessibility(view)) {
            view.post(new Runnable() {
              @Override
              public void run() {
                EasyBehavior.this.startSettlingAnimation(view, state);
              }
            });
          }
        } else {
          this.startSettlingAnimation(view, state);
        }
      }
    }

  }

  private void startSettlingAnimation(View view, int state) {

    int top;
    if (state == STATE_COLLAPSED) {
      top = this.collapsedOffset;
    } else if (state == STATE_EXPANDED) {
      top = this.getExpandedOffset();
    } else if (state == STATE_EXPANDED_MANUAL || state == STATE_ADD_TOP_VIEW) {
      top = view.getTop();
    } else {
      top = this.parentHeight;
    }
    if (this.viewDragHelper.smoothSlideViewTo(view, view.getLeft(), top)) {
      this.setStateInternal(STATE_SETTLING);
      ViewCompat.postOnAnimation(view, new SettleRunnable(view, state));
    } else {
      this.setStateInternal(state);
    }
  }

  public int getParentHeight() {
    return parentHeight;
  }

  public void setParentHeight(int parentHeight) {
    this.parentHeight = parentHeight;
  }

  public BottomSheetCallback getCallback() {
    return callback;
  }

  public void setCallback(BottomSheetCallback callback) {
    this.callback = callback;
  }

  @NotNull
  public static <V extends View> EasyBehavior<V> from(V view) {
    LayoutParams params = view.getLayoutParams();
    if (!(params instanceof android.support.design.widget.CoordinatorLayout.LayoutParams)) {
      throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
    } else {
      Behavior behavior = ((android.support.design.widget.CoordinatorLayout.LayoutParams) params)
          .getBehavior();
      if (!(behavior instanceof EasyBehavior)) {
        throw new IllegalArgumentException("The view is not associated with BottomSheetBehavior");
      } else {
        return (EasyBehavior) behavior;
      }
    }
  }

  protected static class SavedState extends AbsSavedState {

    final int state;
    public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
      public SavedState createFromParcel(Parcel in, ClassLoader loader) {
        return new SavedState(in, loader);
      }

      public SavedState createFromParcel(Parcel in) {
        return new SavedState(in, (ClassLoader) null);
      }

      public SavedState[] newArray(int size) {
        return new SavedState[size];
      }
    };

    public SavedState(Parcel source) {
      this(source, (ClassLoader) null);
    }

    public SavedState(Parcel source, ClassLoader loader) {
      super(source, loader);
      this.state = source.readInt();
    }

    public SavedState(Parcelable superState, int state) {
      super(superState);
      this.state = state;
    }

    public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags);
      out.writeInt(this.state);
    }
  }

  private class SettleRunnable implements Runnable {

    private final View view;
    private final int targetState;

    SettleRunnable(View view, int targetState) {
      this.view = view;
      this.targetState = targetState;
    }

    public void run() {
      if (viewDragHelper != null && viewDragHelper
          .continueSettling(true)) {
        ViewCompat.postOnAnimation(this.view, this);
      } else {
        setStateInternal(this.targetState);
      }

    }
  }

  public interface BottomSheetCallback {

    void onSlide(View view, float value);

    void onStateChanged(View view, int state);
  }


}
